import { ObjectStorage } from './objectStorage';
import OSS from 'ali-oss';
import { Stream } from 'stream';
import { convertStreamToBuffer, convertStreamToString } from '$common';

export interface OssConfig {
  host: string,
  port: number,
  useSSL: boolean,
  accessKey: string,
  accessSecret: string,
  bucket?: string
}

export class OssStorage implements ObjectStorage {

  private ossClient: OSS;

  constructor(private config?: OssConfig) { }

  private async buildOSSClient(): Promise<OSS> {
    const options = {} as OSS.Options;
    if (this.config) {
      options.endpoint = this.config.host;
      options.bucket = this.config.bucket;
      options.accessKeyId = this.config.accessKey;
      options.accessKeySecret = this.config.accessSecret;
    }
    return new OSS(options);
  }

  private async getClient(): Promise<OSS> {
    if (!this.ossClient) {
      this.ossClient = await this.buildOSSClient();
    }
    return this.ossClient;
  }

  async fGetObject(objectName: string, filePath: string): Promise<any> {
    const client = await this.getClient();
    const result = await client.get(objectName, filePath);
    return result;
  }

  async fPutObject(objectName: string, filePath: string): Promise<any> {
    const client = await this.getClient();
    const result = await client.put(objectName, filePath);
    return result;
  }

  async getObjectToStream(objectName: string): Promise<Stream> {
    const client = await this.getClient();
    const result = await client.getStream(objectName);
    return result.stream;
  }

  async getObjectToBuffer(objectName: string): Promise<Buffer> {
    const stream = await this.getObjectToStream(objectName);
    return convertStreamToBuffer(stream);
  }

  async getObjectToString(objectName: string): Promise<string> {
    const stream = await this.getObjectToStream(objectName);
    return await convertStreamToString(stream);
  }

  async putObject(objectName: string, content: string | Buffer): Promise<any> {
    const client = await this.getClient();
    const result = await client.put(objectName, Buffer.from(content));
    return result;
  }

  async deleteObject(objectName: string): Promise<any> {
    const client = await this.getClient();
    const result = await client.delete(objectName);
    return result;
  }

  async append(objectName: string, content: string): Promise<any> {
    let originalContent = '';
    try {
      originalContent = await this.getObjectToString(objectName);
    } catch (e) {
      console.debug(`❗️Storage AppendWarning ==> ${e.code}: ${e.message} objectName: ${objectName}`);
    }
    const data = `${originalContent}${content}`;
    await this.putObject(objectName, data);
  }

  async checkObjectNameExist(objectName: string): Promise<boolean> {
    try {
      const client = await this.getClient();
      await client.head(objectName);
      return true;
    } catch (error) {
      if (error.code === 'NoSuchKey') { return false; }
      throw error;
    }
  }

  async deleteMultiObjects(objectNames: string[]): Promise<void> {
    try {
      const client = await this.getClient();
      await client.deleteMulti(objectNames);
    } catch (error) {
      throw new Error(`Bulk delete failed: ${error.message}`);
    }
  }

  async getLastModified(objectName: string): Promise<Date> {
    const client = await this.getClient();
    const result = await client.head(objectName);
    return new Date(result.res.headers['last-modified']);
  }

  async putObjectWithMetaData(objectName: string, data: any, metaData: object): Promise<void> {
    try {
      const client = await this.getClient();
      return await client.put(objectName, data, {
        headers: this.convertMetaToHeaders(metaData)
      });
    } catch (error) {
      throw new Error(`Put with metadata failed: ${error.message}`);
    }
  }

  private convertMetaToHeaders(meta: object): { [key: string]: string } {
    return Object.entries(meta).reduce((acc, [key, value]) => {
      acc[`x-oss-meta-${key}`] = String(value);
      return acc;
    }, {});
  }

}
